#!/usr/bin/env luajit
-- -*- lua -*-
--
-- Copyright (C) 2014-2020 Markus Klotzbuecher <mk@mkio.de>
--
-- SPDX-License-Identifier: BSD-3-Clause
--

local ubx = require("ubx")
local utils = require("utils")
local strict = require("strict")

local res,lfs = _G.pcall(_G.require,'lfs')
local res,cjson = _G.pcall(_G.require,'cjson')

local ts = tostring
local fmt = string.format

if not res then
   print("error: ubx-modinfo requires LuaFileSystem")
   os.exit(1)
end

local function usage()
   print([[
usage: ubx-modinfo <command> [<args>]
commands:
   list	    	      list available modules

   show MODULE        the contents of a module
     -t TYPE1,TYPE2     show only the given types
     -b BLOCK1,BLOCK2   show only the given blocks

   dump MODULE        dump the module informaton to a file (default: json)
     -f	FORMAT	        desired output format. Supported:
                           json: JSON
                           lua: Lua
                           rest: reStructuredText

global parameters:

   -l N	            set global loglevel to N
   -c [0|1]         color: disable or enable (default)
   -p MOD1,MOD2     list of depending modules to preload
                    (stdtypes loaded by default)
   -h               show help
]])
end

--- Subtract two sets (tables)
-- @param t1 table to substract from
-- @param t2 table to substract
-- @return a table with elements not present in t1
local function table_sub(t1, t2)
   local res = {}
   for k,v in pairs(t1) do
      if t2[k] == nil then res[k] = v end
   end
   return res
end

local function module_license_get(nd, modname)
   local m = ubx.module_get(nd, modname)
   if m == nil then error("unknown module "..modname) end
   return ubx.safe_tostr(m.spdx_license_id)
end
--- extract a modules content
-- @param module name
-- @param premods modules to preload
-- @param loglevel loglevel to use for loading
-- @return table of blocks
-- @return table of types
local function module_totab(mod, premods, loglevel)
   local nd = ubx.node_create("node", { loglevel=loglevel })

   if mod ~= "stdtypes" then
      table.insert(premods, 1, "stdtypes")
   end

   for _,m in ipairs(premods) do ubx.load_module(nd, m) end

   local n1 = ubx.node_totab(nd)
   local modid = ubx.load_module(nd, mod)
   local n2 = ubx.node_totab(nd)

   local types = table_sub(n2.types, n1.types)
   local blocks = table_sub(n2.blocks, n1.blocks)
   local spdx_license = module_license_get(nd, modid)
   return blocks, types, spdx_license
end

-- markup with reStr monospace if not empty
local function mono_if_not_empty(s)
   if type(s)=='string' and s ~= '' then return "``"..s.."``" end
   return ''
end

local function modtab_to_rest(mod, blocks, types, license)
   local res, str = utils.preproc(
[[
@ local modhdr = string.format("Module %s", mod)
$(modhdr)
$(string.rep("-", #modhdr))

@ for bn,b in pairs(blocks or {}) do
@ if not output_block_hdr then
@ output_block_hdr = true
@ end
Block $(bn)
$(string.rep("^", #("Block "..bn)))

| **Type**:       $(b.block_type)
| **Attributes**: $(table.concat(b.attrs, ', '))
| **Meta-data**:  $(b.meta_data)
| **License**:    $(license)

@ for _,c in pairs(b.configs) do
@   if not output_cfg_hdr then
@     output_cfg_hdr = true

Configs
"""""""

.. csv-table::
   :header: "name", "type", "doc"

@   end
   $(c.name), ``$(c.type_name)``, "$(c.doc)"
@ end


@ for _,p in pairs(b.ports) do
@   if not output_port_hdr then
@     output_port_hdr = true

Ports
"""""

.. csv-table::
   :header: "name", "out type", "out len", "in type", "in len", "doc"

@   end
@   if p.out_type_name then p.out_type_len = p.out_type_len or 1 end
@   if p.in_type_name then p.in_type_len = p.in_type_len or 1 end
   $(p.name), $(mine(p.out_type_name)), $(p.out_type_len), $(mine(p.in_type_name)), $(p.in_type_len), "$(p.doc)"
@ end
@ end

@ for tn,t in pairs(types) do
@ if not output_type_hdr then
@ output_type_hdr = true
Types
^^^^^

.. csv-table:: Types
   :header: "type name", "type class", "size [B]"

@ end
   ``$(t.name)``, $(t.class), $(t.size)
@ end

]], { mod=mod, blocks=blocks, types=types, license=license,
      table=table, string=string, pairs=pairs,
      mine=mono_if_not_empty } )

   if not res then error(str) end
   return str
end

--- list all modules found in the current path
local function modules_list()
   local _,prefixes = ubx.get_prefix()
   local ver = string.sub(ubx.safe_tostr(ubx.version()), 1, 3)

   for _,prefix in ipairs(prefixes) do
      local modpath = prefix.."/lib/ubx/"..ver.."/"

      if utils.file_exists(modpath) then
	 for f in lfs.dir(modpath) do
	    local ext = string.match(f, "^.+%.(.+)$")
	    if f ~= "." and f ~= ".." and ext == "so" then
	       print(modpath..f)
	    end
	 end
      end
   end
end

--- get_arg helper
-- @param opttab opttab converter table
-- @param optname name of option (e.g. "-v")
-- @param defarg default value for non mandatory optargs
-- @param req this option is required
-- @param reqarg requires an option argument
-- @param typecheck function to convert/check the argument
local function get_arg(opttab, optname, defarg, req, reqarg, typecheck)
   typecheck = typecheck or function(x) return x end

   if defarg and req and reqarg then
      error("required optarg with default value makes no sense")
   end

   if req and (not opttab[optname]) then
      print("error: mandatory option "..optname.. " missing")
      os.exit(1)
   end

   -- arg-value required but not there?
   if reqarg and opttab[optname] and (not opttab[optname][1]) then
      print("error: option "..optname.. " requires an argument")
      os.exit(1)
   end

   -- arg not there at all
   -- we also return the default if the opt is not given at all
   if not opttab[optname] or (not opttab[optname][1]) then
      return defarg
   end

   return typecheck(opttab[optname][1])
end


--- prog starts here
if #arg < 1 then
   usage(); os.exit(1)
end

local opttab = utils.proc_args(arg)

if opttab['-h'] then
   usage(); os.exit(0)
end

-- handle global args
local loglevel = get_arg(opttab, "-l", 6, false, false, tonumber)

ubx.color = get_arg(opttab, "-c", true, false, true,
		    function (c) return tonumber(c) ~= 0 end)

if opttab[0][1] == "list" then
   modules_list()
   os.exit(0)
end

local premods = get_arg(opttab, "-p", {}, false, true,
			function(modlist) return utils.split(modlist, ',') end)

if opttab[0][1] == "show" then
   if not opttab[0][2] then
      print("show requires a module parameter")
      os.exit(1)
   end

   local mod = opttab[0][2]

   local show_blocks = get_arg(opttab, "-b", {}, false, true,
			       function(modlist) return utils.split(modlist, ',') end)

   local show_types = get_arg(opttab, "-t", {}, false, true,
			      function(typelist) return utils.split(typelist, ',') end)

   local blocks, types, license = module_totab(mod, premods, loglevel)

   print("module " .. mod)
   print(" license: " ..ts(license)..'\n')

   if #show_types > 0 or #show_blocks > 0 then
      if #show_types > 0 then
	 for _,tname in ipairs(show_types) do
	    print("  " ..ubx.type_tostr(types[tname]))
	 end
      end

      if #show_blocks > 0 then
	 for _,bname in ipairs(show_blocks) do
	    print("  " ..ubx.block_pp(blocks[bname]))
	 end
      end
   else
      -- print all
      print(" types:")
      for _,t in pairs(types) do print("  "..ubx.type_tostr(t)) end
      print("\n blocks:")
      for _,b in pairs(blocks) do print("  " ..ubx.block_pp(b)) end
   end
elseif opttab[0][1] == "dump" then
   if not opttab[0][2] then
      print("dump requires a module parameter")
      os.exit(1)
   end

   local mod = opttab[0][2]
   local format = get_arg(opttab, "-f", 'json', false, true)
   local blocks, types, license = module_totab(mod, premods)

   if format == 'json' then
      print(cjson.encode{ types=types, blocks=blocks, license=license })
   elseif format == 'lua' then
      print(utils.tab2str{ types=types, blocks=blocks, license=license })
   elseif format == 'rest' then
      print(modtab_to_rest(mod, blocks, types, license))
   else
      print(fmt("invalid format %s", format))
      os.exit(1)
   end
else
   print(fmt("unknown command %s", opttab[0][1]))
   os.exit(1)
end
